<?php

namespace App\Lib\token;

use App\Exceptions\lin\TokenExpiredException;
use App\Exceptions\lin\TokenException;
use Exception;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\ValidationData;

class Token
{
    /**
     * 生成令牌
     * @param TokenConfig $tokenConfig
     * @return array
     * @throws Exception
     */
    public static function makeToken(TokenConfig $tokenConfig): array
    {
        $result = [];
        $payload = self::generatePayload($tokenConfig);

        $accessToken = self::encode($payload['accessPayload'], $tokenConfig->getAccessSecretKey(), $tokenConfig->getAlgorithms());
        $result['accessToken'] = $accessToken;

        if ($tokenConfig->isDualToken()) {
            $refreshToken = self::encode($payload['refreshPayload'], $tokenConfig->getRefreshSecretKey(), $tokenConfig->getAlgorithms());
            $result['refreshToken'] = $refreshToken;
        }
        return $result;
    }

    /**
     * 刷新令牌
     * @param string $token
     * @param TokenConfig $tokenConfig
     * @return array
     * @throws TokenException|TokenExpiredException
     */
    public static function refresh(string $token, TokenConfig $tokenConfig): array
    {
        $tokenPayload = self::verifyToken($token, 'refresh', $tokenConfig);
//        $tokenPayload['aud'] = $tokenConfig->getAud();
//        $tokenPayload['jti'] = $tokenConfig->getJti();
        $tokenPayload['iat'] = $tokenConfig->getIat();
        $tokenPayload['exp'] = $tokenPayload['iat'] + $tokenConfig->getAccessExp();
        $token = self::encode((array)$tokenPayload, $tokenConfig->getAccessSecretKey(), $tokenConfig->getAlgorithms());
        return [
            'accessToken' => $token
        ];
    }

    /**
     * 令牌校验
     * @param string $token
     * @param string $tokenType
     * @param TokenConfig $tokenConfig
     * @return array|mixed 令牌解密后的内容
     * @throws TokenException|TokenExpiredException
     */
    public static function verifyToken(string $token, string $tokenType, TokenConfig $tokenConfig): array
    {
        try {
            $decodeToken = self::decode($token, $tokenConfig);
        } catch (Exception $e) {
            throw new TokenException();
        }
        $secretKey = $tokenType === 'access' ? $tokenConfig->getAccessSecretKey() : $tokenConfig->getRefreshSecretKey();
        //验证是否被修改
        $verify = self::verify($secretKey, $tokenConfig);
        if (empty($verify)) throw new TokenException();
        //验证有效期
        $validate = self::validate($tokenConfig);
        if (empty($validate)) throw new TokenExpiredException();

        return $decodeToken;
    }

    /**
     * 加密jwt
     * @param array $payload
     * @param string $secretKey
     * @param string $algorithms
     * @return string
     */
    private static function encode(array $payload, string $secretKey, string $algorithms): string
    {
        return (new Builder())
            ->setHeader("alg", $algorithms) //算法签名
            ->setIssuer($payload['iss'])// 签发人
//            ->setAudience($payload['aud'])// 配置访问群体
//            ->setId($payload['jti'], true)// 配置id（jti声明），作为头项进行复制
            ->setIssuedAt($payload['iat'])// 配置令牌的颁发时间（iat声明）
//            ->setNotBefore($payload['iat'] + 1)// 配置令牌可以使用的时间（单位:分钟）1分钟
            ->set("extend", $payload['extend'])//
            ->setExpiration($payload['exp'])// 配置令牌的过期时间 (单位:秒) 60秒
            ->sign(new Sha256(), $secretKey)// 使用secrect作为密钥创建签名
            ->getToken(); // 检索生成的令牌
    }

    /**
     * 解密jwt
     * @param $token
     * @param TokenConfig $tokenConfig
     * @return mixed
     */
    private static function decode($token, TokenConfig $tokenConfig)
    {
        if (!$tokenConfig->getDecodeToken()) {
            $tokenConfig->setToken($token);
            $decodeToken = (new Parser())->parse((string)$tokenConfig->getToken());
            $tokenConfig->setDecodeToken($decodeToken);
        }
        # 返回签名时间以及过期时间、扩展信息
        return [
            'iss' => $tokenConfig->getDecodeToken()->getClaim('iss'),
            'iat' => $tokenConfig->getDecodeToken()->getClaim('iat'),
            'extend' => (array)$tokenConfig->getDecodeToken()->getClaim('extend'),
            'exp' => $tokenConfig->getDecodeToken()->getClaim('exp'),
        ];
    }

    /**
     * 验证令牌在生成后是否被修改
     * @param $secretKey
     * @param TokenConfig $tokenConfig
     * @return mixed
     */
    private static function verify($secretKey, TokenConfig $tokenConfig)
    {
        return $tokenConfig->getDecodeToken()->verify(new Sha256(), $secretKey);
    }

    /**
     * 验证令牌是否超过有效期
     * @param TokenConfig $tokenConfig
     * @return mixed
     */
    private static function validate(TokenConfig $tokenConfig)
    {
        $data = new ValidationData();
        $data->setIssuer($tokenConfig->getIss());
//        $data->setAudience($tokenConfig->getAud());
//        $data->setId($tokenConfig->getJti());
        return $tokenConfig->getDecodeToken()->validate($data);
    }

    /**
     * 产生有效载荷
     * @param TokenConfig $tokenConfig
     * @return array[]
     * @throws Exception
     */
    private static function generatePayload(TokenConfig $tokenConfig): array
    {
        $tokenConfig->checkParams();
        $resPayLoad = [
            'accessPayload' => [],
            'refreshPayload' => [],
        ];
        $basePayload = [
            'iss' => $tokenConfig->getIss(), //签发者
//            'aud' => $tokenConfig->getAud(), //访问群体
//            'jti' => $tokenConfig->getJti(), //唯一身份标识
            'iat' => $tokenConfig->getIat(), //什么时候签发的
            'extend' => $tokenConfig->getExtend()  //扩展信息
        ];
        $basePayload['exp'] = $tokenConfig->getIat() + $tokenConfig->getAccessExp();
        $resPayLoad['accessPayload'] = $basePayload;
        if ($tokenConfig->isDualToken()) { //双令牌设置
            $basePayload['exp'] = time() + $tokenConfig->getRefreshExp();
            $resPayLoad['refreshPayload'] = $basePayload;
        }
        return $resPayLoad;
    }

    /**
     * 单例模式 禁止外部克隆
     */
    private function __clone()
    {
        // TODO: Implement __clone() method.
    }
}
